package org.example;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;

class QTreeFromGrid {
    QTreeFromGrid() {}
    // 从二进制文件中读取地图
    public boolean[][] readMapBinary(String path) throws IOException {
        final var size = 512;
        var dis = new DataInputStream(new FileInputStream(path));
        var gridR = new boolean[512][512];
        try {
            var buf = new byte[size * size];
            dis.readFully(buf);
            for (int i=0; i!=size; i++) {
                for (int j=0; j!=size; j++) {
                    // !!! 注意注意注意 !!!
                    // 这里gridR[j][i]而不是gridR[i][j]的原因是，在二进制数据中像素是按行保存的，假设原始像素数据如下：
                    // 行1:   1 2 3 4 ... 512
                    // 行2:   1 2 3 4 ... 512
                    // 行3:   1 2 3 4 ... 512
                    // ...
                    // 行512: 1 2 3 4 ... 512
                    // 保存到二进制文件中时按顺序写入第一行的512字节、第二行的512字节以此类推。
                    // 那么在恢复数据时，在这个循环中理应用gridR[i][j] = (buf[i * size + j] == 1);
                    // 这样做也是没有问题的。此时我们取(x,y)坐标的点时，需要访问grid的第x列第y行。即访问grid[y][x]。
                    // 不过因为我更喜欢grid[x][y]这种更直观的方式，所以这里把坐标做了交换。
                    gridR[j][i] = (buf[i * size + j] == 1);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            dis.close();
        }
        return gridR;
    }

    /**
     * 手工解析npy文件。npy文件带有80h个额外的头数据，这里直接丢弃。
     */
    public boolean[][] readNpy(String path) throws IOException {
        final var size = 512;
        var dis = new DataInputStream(new FileInputStream(path));
        var gridR = new boolean[512][512];
        try {
            var buf = new byte[size * size + 0x80];
            dis.readFully(buf);
            for (int i=0; i!=size; i++) {
                for (int j=0; j!=size; j++) {
                    // !!! 注意注意注意 !!!解释gridR[j][i]而不是gridR[i][j]的原因。
                    // 在二进制数据中像素是按行保存的，假设原始像素数据如下：
                    // 行1:   1 2 3 4 ... 512
                    // 行2:   1 2 3 4 ... 512
                    // 行3:   1 2 3 4 ... 512
                    // ...
                    // 行512: 1 2 3 4 ... 512
                    // 保存到二进制文件中时按顺序写入第一行的512字节、第二行的512字节以此类推。
                    // 那么在恢复数据时，在这个循环中理应用gridR[i][j] = (buf[i * size + j] == 1);
                    // 这样做也是没有问题的。此时我们取(x,y)坐标的点时，需要访问二维数组的第x列第y行。即访问grid[y][x]。
                    // 因为我更喜欢用grid[x][y]这种更直观的表达方式，所以这里把坐标做了交换。
                    gridR[j][i] = (buf[i * size + j + 0x80] == 1);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            dis.close();
        }
        return gridR;
    }

    // 检查区域是否同质（全黑或者全白）
    public boolean isUniform(boolean[][] grid, int x, int y, int size) {
        var first = grid[x][y];
        for (int i = x; i != x + size; i ++) {
            for(int j = y; j != y + size; j++) {
                if (grid[i][j] != first)
                    return false;
            }
        }
        return true;
    }

    // 递归构建四叉树
    public QuadTree buildQuadTree(boolean[][] grid, int x, int y, int size) {
        if (isUniform(grid, x, y, size)) {
            return new Leaf(new Bounds(x, y, size),  !grid[x][y]);
        }
        else
        {
            final var half = size/2;
            return new TreeNode(
                    buildQuadTree(grid, x, y, half),
                    buildQuadTree(grid, x, y+half, half),
                    buildQuadTree(grid, x+half, y, half),
                    buildQuadTree(grid, x+half, y+half, half),
                    new Bounds(x, y, size)
            );
        }
    }

    // 为整个四叉树计算所有节点的邻居
    private void traverse(QuadTree node, QuadTree root) {
        if (node.getClass() == TreeNode.class) {
            traverse(((TreeNode) node).nw, root);
            traverse(((TreeNode) node).ne, root);
            traverse(((TreeNode) node).sw, root);
            traverse(((TreeNode) node).se, root);
        }
        else if (node.getClass() == Leaf.class) {
            Leaf l = (Leaf) node;
            if (!l.isObstacle) {
                l.findNeighbors(root);
            }
        }
    }

    public void computeAllNeighbors(QuadTree root) {
        traverse(root, root);
    }

    public static void test1() {
        System.out.println("You are now in QTreeFromGrid.main");
        System.out.println("We are going to test:");
        System.out.println("- load 512x512 map data from grids.data");
        System.out.println("- build QTree from grid data loaded");
        System.out.println("- compute all leaf neighbors in this QTree");
        System.out.println("- serialize QTree to the file serializeBinary.data");
        System.out.println("- test AStar path routing.");
        System.out.println("- visualize QTree struct, neighborhoods, path finding in grids.png");

        final var mapSize = 512;

        var obj = new QTreeFromGrid();
        try {
            // load 512x512 map data from grids.data
            var grid = obj.readMapBinary("D:\\temp\\jl\\grids.data");

            // build QTree from grid data loaded
            var quadTree = obj.buildQuadTree(grid, 0,0, mapSize);

            // compute all leaf neighbors in this QTree
            obj.computeAllNeighbors(quadTree);

            // serialize QTree to the file javaSerialize.data
            var seri = new QTreeSerializer();
            seri.serializeCustom(quadTree, "D:\\temp\\jl\\javaSerialize.data");
//            // test AStar path routing.
//            val route = AStar.findPath(quadTree, 110, 338, 484, 372)
//            println(route)
            new QTreeRender().renderQuadTree(quadTree, 512, "D:\\temp\\jl\\grids3.png", new ArrayList<>());
            seri.deserializeCustom("D:\\temp\\jl\\javaSerialize.data");
        } catch (IOException e) {
            System.out.println("Exception:" + e);
        }
    }

    public static void test2() {
        System.out.println("You are now in QTreeFromGrid.main");
        System.out.println("We are going to test:");
        System.out.println("- load 512x512 map data from grids.data");
        System.out.println("- build QTree from grid data loaded");
        System.out.println("- compute all leaf neighbors in this QTree");
        System.out.println("- serialize QTree to the file serializeBinary.data");
        System.out.println("- test AStar path routing.");
        System.out.println("- visualize QTree struct, neighborhoods, path finding in grids.png");

        final var mapSize = 512;

        var obj = new QTreeFromGrid();
        try {
            // load 512x512 map data from grids.data
            var grid = obj.readNpy("data/pytile/0_1024.npy");

            // build QTree from grid data loaded
            var quadTree = obj.buildQuadTree(grid, 0,0, mapSize);

            // compute all leaf neighbors in this QTree
            obj.computeAllNeighbors(quadTree);

            // serialize QTree to the file javaSerialize.data
            var seri = new QTreeSerializer();
            seri.serializeCustom(quadTree, "data/javaSerialize.data");
//            // test AStar path routing.
//            val route = AStar.findPath(quadTree, 110, 338, 484, 372)
//            println(route)
            new QTreeRender().renderQuadTree(quadTree, 512, "data/grids3.png", new ArrayList<>());
            seri.deserializeCustom("data/javaSerialize.data");
        } catch (IOException e) {
            System.out.println("Exception:" + e);
        }
    }

    public static void main(String[] args) {
        test2();
    }
}